//
// Created by martin on 4/2/22.
//

#include "FtpDataServer.h"
#include "DataAcceptor.h"
#include "muduo/base/Logging.h"
#include "muduo/net/SocketsOps.h"

#include <functional>

using namespace ftp;
using namespace muduo;
using namespace muduo::net;

muduo::AtomicInt64 FtpDataServer::nextConnId_;

FtpDataServer::FtpDataServer(muduo::net::EventLoop* loop,
	const muduo::net::InetAddress& serverAddr,
	const std::string& name,
	muduo::net::TcpServer::Option option)
	: loop_(CHECK_NOTNULL(loop)),
	serverAddr_(serverAddr),
	name_(name),
	autoDataPortFlag_(serverAddr.port() == 0 ? true : false),
	acceptor_(new DataAcceptor(loop_, serverAddr, false)),
	minDataPort_(kDefatulMinPort),
	maxDataPort_(kDefatulMaxPort)
{
    profile();
	acceptor_->setNewConnectionCallback(std::bind(&FtpDataServer::newConnection, this, _1, _2));
}

FtpDataServer::~FtpDataServer()
{
	// Why not invoke shutdown(), disconnect(),  or forceClose() of TcpConnection?
	// Because we need to destroy TcpConnection managed by this Server directly.
	// 
	// shutdown() just shut down write direction, not reading direction. The server 
	// will wait for FIN segment from client to call back TcpConnection::handleClose() to 
	// close tcp connection. If the server do not receive FIN from the client, the connection
	// won't be closed completely.
	// It only works for the connection in state kConnected, then go to state kDisconnecting.
	// 
	//
	// forceClose() invoke TcpConnection::handleClose() to disable all channel events, and 
	// callback connectionCallback_, closeCallback_(defined by user).
	// It only works for the connection in state kConnected or kDisconnecting, then go to 
	// state kDisconnected.
	// 
	// We often bind closeCallback_ to removeConnection() user defined, which invoke 
	// TcpConnection::connectDestroyed() to destroy a connection.
	//

	loop_->assertInLoopThread();
	LOG_DEBUG << "Destroy FtpDataServer obj@" << this;

	TcpConnectionPtr conn(connection_);
	connection_.reset();
	if (conn) {
		conn->getLoop()->runInLoop(std::bind(&TcpConnection::connectDestroyed, conn));
	}
}

/**
 * Call setDataPortRange() before start() if need to change the default data port range.
 */
void FtpDataServer::start()
{
	loop_->assertInLoopThread();
	uint16_t port = 0, minPort = 0, maxPort = 0;
	if (autoDataPortFlag_) { // auto bind port decided by core
		port = 0;
		minPort = port;
		maxPort = port;
	}
	else {
		port = static_cast<uint16_t>(maxDataPort_);
		minPort = static_cast<uint16_t>(minDataPort_);
		maxPort = static_cast<uint16_t>(maxDataPort_);
	}

	InetAddress addr(serverAddr_.toIp(), port);
	acceptor_->createSocketAndListen(serverAddr_);
	if (acceptor_->listening()) {
		serverAddr_ = acceptor_->listenAddr();
	}

	assert(acceptor_->listening());
}

bool FtpDataServer::listening() const
{
	return acceptor_->listening();
}

/**
 * @brief Change the default data port range the Ftp Data Server will listen on.
 *
 * Port 0 means application does not care for port number, and it will be a random port
 * decided by kernel automatically. The kernel will ensure the uniqueness of the port.
 * The default random port range is [1024, 65535].
 *
 * Suggest application DO NOT change the default port range except a necessary reason.
 * Because one process can bind/listen the same port over two times using different sockfd,
 * that is, the process can not ensure the uniqueness of the port by specifying the port
 * range manually .
 *
 * Under Unix/Linux, listening on port 0~1023 requires root previledge.
 *
 * @param minPort: min port number to find out an available listening port manually
 *
 * @param maxPort: max port number to find out an available listening port manually
 */
void FtpDataServer::setDataPortRange(uint16_t minPort, uint16_t maxPort)
{
	if (minDataPort_ > maxPort) return; // invalide param

	if (maxPort != 0 && (minPort != minDataPort_ || maxPort != maxDataPort_)) {
		autoDataPortFlag_ = false;
		minDataPort_ = minPort;
		maxDataPort_ = maxPort;
	}
}

void FtpDataServer::shutdown()
{
	if (get_pointer(connection_)) {
		connection_->shutdown();
	}
}

void FtpDataServer::disconnect()
{
	if (get_pointer(connection_)) {
		connection_->forceClose();
	}
}

InetAddress FtpDataServer::listenAddress() const
{
	if (get_pointer(acceptor_))
		return acceptor_->listenAddr();
	return InetAddress();
}

void FtpDataServer::newConnection(int sockfd, const muduo::net::InetAddress& peerAddr)
{
	loop_->assertInLoopThread();
	EventLoop* ioLoop = loop_;

	char buf[32];
	snprintf(buf, sizeof(buf), "-%s#%ld", ipPort_.c_str(), nextConnId_.get());
	nextConnId_.increment();
	std::string connName = name_ + buf;

	LOG_INFO << "FtpDataServer::newConnection [" << name_
		<< "] - new connection [" << connName
		<< "] from " << peerAddr.toIpPort();

	InetAddress localAddr(sockets::getLocalAddr(sockfd)); // get local ip address, port info
	TcpConnectionPtr conn(new TcpConnection(ioLoop, connName, sockfd, localAddr, peerAddr));
	connection_ = conn;
	// Set callbacks for new TcpConnection object.
	// Require this TcpConnection object is destroy earlier than FtpDataServer object, 
	// or the behavior deliver this to callback is unsafe.
	conn->setConnectionCallback(connectionCallback_);
	conn->setMessageCallback(messageCallback_);
	conn->setWriteCompleteCallback(writeCompleteCallback_);
	// delay this object's releasing, after tcp connection(conn) closing
	conn->setCloseCallback(std::bind(&FtpDataServer::removeConnection, shared_from_this(), _1));

	conn->setTcpNoDelay(true); // disable Nagle algorithm
	ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}

void FtpDataServer::removeConnection(const muduo::net::TcpConnectionPtr& conn)
{
	loop_->runInLoop(
		std::bind(&FtpDataServer::removeConnectionInLoop, this, conn));
}

void FtpDataServer::removeConnectionInLoop(const muduo::net::TcpConnectionPtr& conn)
{
	loop_->assertInLoopThread();
	LOG_INFO << "FtpDataServer::removeConnectionInLoop [" << name_
		<< "] - connection " << conn->name();

	if (connection_) {
		connection_.reset();
		EventLoop* ioLoop = loop_;

		// ! Very important to invoke TcpConnection::connectDestroyed() to ensure 
		// connection would be destroyed: disable and remove all channel events
		// listened on poll/epoll of the EventLoop.
		ioLoop->queueInLoop(
			std::bind(&TcpConnection::connectDestroyed, conn));
	}
}

void FtpDataServer::profile()
{
    LOG_DEBUG << "===FtpDataServer obj@===" << this
    << "loop=" << loop_ << ", ipPort" << ipPort_ << ", name=" << name_
    << ", server address=" << serverAddr_.toIpPort() << ", autoDataPortFlag=" << autoDataPortFlag_
    << ", minDataPort_=" << minDataPort_ << ", maxDataPort_=" << maxDataPort_;
}

#if 0
void FtpDataServer::onConnection(const TcpConnectionPtr& conn)
{
	if (connectionCallback_) {
		connectionCallback_(conn);
	}
}

void FtpDataServer::onMessage(const TcpConnectionPtr& conn, Buffer* buffer, Timestamp receiveTime)
{
	if (messageCallback_) {
		messageCallback_(conn, buffer, receiveTime);
	}
}

void FtpDataServer::onWriteComplete(const TcpConnectionPtr& conn)
{
	if (writeCompleteCallback_) {
		writeCompleteCallback_(conn);
	}
}
#endif
